var __createBinding =
    (this && this.__createBinding) ||
    (Object.create
        ? (o, m, k, k2) => {
              if (k2 === undefined) k2 = k;
              var desc = Object.getOwnPropertyDescriptor(m, k);
              if (
                  !desc ||
                  ("get" in desc
                      ? !m.__esModule
                      : desc.writable || desc.configurable)
              ) {
                  desc = { enumerable: true, get: () => m[k] };
              }
              Object.defineProperty(o, k2, desc);
          }
        : (o, m, k, k2) => {
              if (k2 === undefined) k2 = k;
              o[k2] = m[k];
          });
var __setModuleDefault =
    (this && this.__setModuleDefault) ||
    (Object.create
        ? (o, v) => {
              Object.defineProperty(o, "default", {
                  enumerable: true,
                  value: v,
              });
          }
        : (o, v) => {
              o.default = v;
          });
var __importStar =
    (this && this.__importStar) ||
    ((mod) => {
        if (mod?.__esModule) return mod;
        var result = {};
        if (mod != null)
            for (var k in mod)
                if (k !== "default" && Object.hasOwn(mod, k))
                    __createBinding(result, mod, k);
        __setModuleDefault(result, mod);
        return result;
    });
var __importDefault =
    (this && this.__importDefault) ||
    ((mod) => (mod?.__esModule ? mod : { default: mod }));
Object.defineProperty(exports, "__esModule", { value: true });
exports.StringRevealer = void 0;
const t = __importStar(require("@babel/types"));
const traverse_1 = __importDefault(require("@babel/traverse"));
const transformation_1 = require("../transformation");
const declaration_1 = require("../../helpers/declaration");
const basicStringDecoder_1 = require("../../helpers/strings/decoders/basicStringDecoder");
const generator_1 = __importDefault(require("@babel/generator"));
const rc4StringDecoder_1 = require("../../helpers/strings/decoders/rc4StringDecoder");
const stringDecoder_1 = require("../../helpers/strings/decoders/stringDecoder");
const base64StringDecoder_1 = require("../../helpers/strings/decoders/base64StringDecoder");
const rotation_1 = require("../../helpers/strings/rotation/rotation");
const BASE_64_WRAPPER_REGEX =
    /['"]abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\+\/=['"]\.indexOf/;
const RC4_WRAPPER_REGEX =
    /[a-zA-Z$_]?[a-zA-Z0-9$_]+\s?\+=\s?String\.fromCharCode\([a-zA-Z$_]?[a-zA-Z0-9$_]+\.charCodeAt\([a-zA-Z$_]?[a-zA-Z0-9$_]+\)\s?\^\s?[a-zA-Z$_]?[a-zA-Z0-9$_]+\[\([a-zA-Z$_]?[a-zA-Z0-9$_]+\[[a-zA-Z$_]?[a-zA-Z0-9$_]+\]\s?\+\s?[a-zA-Z$_]?[a-zA-Z0-9$_]+\[[a-zA-Z$_]?[a-zA-Z0-9$_]+\]\)\s?%\s?(?:256|0x100)\]\)/;
class StringRevealer extends transformation_1.Transformation {
    /**
     * Executes the transformation.
     * @param log The log function.
     */
    execute(log) {
        const self = this;
        (0, traverse_1.default)(this.ast, {
            enter(path) {
                if (self.isStringArrayFunction(path.node)) {
                    let stringArray;
                    if (t.isVariableDeclaration(path.node.body.body[0])) {
                        const arrayExpression =
                            path.node.body.body[0].declarations[0].init;
                        stringArray = arrayExpression.elements.map(
                            (e) => e.value,
                        );
                    } else {
                        const string =
                            path.node.body.body[0].expression.right.callee
                                .object.value;
                        const separator =
                            path.node.body.body[0].expression.right.arguments[0]
                                .value;
                        stringArray = string.split(separator);
                    }
                    const arrayName = path.node.id.name;
                    const binding = path.scope.getBinding(arrayName);
                    if (!binding) {
                        return;
                    }
                    const wrapperFunctions = [];
                    const stringDecoders = [];
                    let rotateCall;
                    for (const referencePath of binding.referencePaths) {
                        // ignore call to function from within
                        if (referencePath.scope === path.scope) {
                            continue;
                        }
                        if (referencePath.parentKey === "callee") {
                            const functionParent =
                                referencePath.getFunctionParent();
                            if (!functionParent) {
                                return;
                            }
                            if (
                                self.isBasicStringArrayWrapper(
                                    functionParent.node,
                                    arrayName,
                                )
                            ) {
                                const offsetExpression =
                                    functionParent.node.body.body[1].expression
                                        .right.body.body[0].expression.right;
                                const absoluteOffset =
                                    offsetExpression.right.value;
                                const offset =
                                    offsetExpression.operator === "+"
                                        ? absoluteOffset
                                        : -absoluteOffset;
                                const decoder =
                                    new basicStringDecoder_1.BasicStringDecoder(
                                        stringArray,
                                        offset,
                                    );
                                stringDecoders.push(decoder);
                                wrapperFunctions.push(functionParent);
                            } else if (
                                self.isComplexStringArrayWrapper(
                                    functionParent.node,
                                    arrayName,
                                )
                            ) {
                                const offsetExpression =
                                    functionParent.node.body.body[1].expression
                                        .right.body.body[0].expression.right;
                                const absoluteOffset =
                                    offsetExpression.right.value;
                                const offset =
                                    offsetExpression.operator === "+"
                                        ? absoluteOffset
                                        : -absoluteOffset;
                                const src = (0, generator_1.default)(
                                    functionParent.node,
                                ).code;
                                if (BASE_64_WRAPPER_REGEX.test(src)) {
                                    if (RC4_WRAPPER_REGEX.test(src)) {
                                        const decoder =
                                            new rc4StringDecoder_1.Rc4StringDecoder(
                                                stringArray,
                                                offset,
                                            );
                                        stringDecoders.push(decoder);
                                    } else {
                                        const decoder =
                                            new base64StringDecoder_1.Base64StringDecoder(
                                                stringArray,
                                                offset,
                                            );
                                        stringDecoders.push(decoder);
                                    }
                                    wrapperFunctions.push(functionParent);
                                } else {
                                    return; // unknown string array wrapper type
                                }
                            } else {
                                return; // unknown reference to string array function found
                            }
                        } else if (referencePath.parentKey === "arguments") {
                            const parentPath = referencePath.parentPath;
                            if (
                                self.isRotateStringArrayCall(
                                    parentPath.node,
                                    arrayName,
                                )
                            ) {
                                rotateCall = parentPath.parentPath;
                            } else {
                                return; // unknown reference to string array function found
                            }
                        } else {
                            return; // unknown reference to string array function found
                        }
                    }
                    // ensure there is at least one wrapper function
                    if (wrapperFunctions.length === 0) {
                        return;
                    }
                    const wrapperFunctionNames = wrapperFunctions.map(
                        (w) => w.node.id.name,
                    );
                    const wrapperBindings = wrapperFunctions.map((w, i) =>
                        w.scope.getBinding(wrapperFunctionNames[i]),
                    );
                    if (wrapperBindings.find((w) => !w)) {
                        log(
                            `Failed to find string concealer wrapper functions`,
                        );
                        return;
                    }
                    // perform string rotation if necessary
                    if (rotateCall) {
                        const stopValue =
                            rotateCall.node.expression.arguments[1].value;
                        const body =
                            rotateCall.node.expression.callee.body.body;
                        const loop = body[body.length - 1];
                        const statement = loop.body.body[0].block.body[0];
                        const expression = t.isVariableDeclaration(statement)
                            ? statement.declarations[0].init
                            : statement.expression.right;
                        const decoderMap = new Map(
                            stringDecoders.map((decoder, index) => [
                                wrapperFunctionNames[index],
                                decoder,
                            ]),
                        );
                        (0, rotation_1.rotateStringArray)(
                            stringArray,
                            expression,
                            decoderMap,
                            stopValue,
                        );
                    }
                    let failedReplacement = false;
                    for (let i = 0; i < wrapperFunctions.length; i++) {
                        const wrapperFunction = wrapperFunctions[i];
                        const wrapperBinding = wrapperBindings[i];
                        const decoder = stringDecoders[i];
                        for (const referencePath of wrapperBinding.referencePaths) {
                            const functionParent =
                                referencePath.getFunctionParent();
                            const outerFunctionParent =
                                functionParent?.getFunctionParent();
                            const parentPath = referencePath.parentPath;
                            if (
                                (functionParent &&
                                    (functionParent.node ===
                                        wrapperFunction.node ||
                                        (rotateCall &&
                                            functionParent.node ===
                                                rotateCall.node.expression
                                                    .callee))) ||
                                (outerFunctionParent &&
                                    outerFunctionParent.node ===
                                        wrapperFunction.node)
                            ) {
                            } else if (
                                !parentPath ||
                                !self.isStringArrayWrapperCall(
                                    parentPath.node,
                                    decoder.type,
                                )
                            ) {
                                failedReplacement = true;
                            } else {
                                try {
                                    const args = parentPath.node.arguments.map(
                                        (a) => a.value,
                                    );
                                    const value = decoder.getString(...args);
                                    if (typeof value === "string") {
                                        parentPath.replaceWith(
                                            t.stringLiteral(value),
                                        );
                                        self.setChanged();
                                    } else {
                                        failedReplacement = true;
                                    }
                                } catch (_err) {
                                    failedReplacement = true;
                                }
                            }
                        }
                    }
                    if (!failedReplacement) {
                        path.remove();
                        for (const wrapper of wrapperFunctions) {
                            wrapper.remove();
                        }
                        if (rotateCall) {
                            rotateCall.remove();
                        }
                        self.setChanged();
                    }
                } else if (self.isEscapedStringLiteral(path.node)) {
                    path.node.extra = undefined;
                    self.setChanged();
                }
            },
        });
        return this.hasChanged();
    }
    /**
     * Returns whether a node is the function that splits and returns the
     * string array.
     * @param node The AST node.
     * @returns Whether.
     */
    isStringArrayFunction(node) {
        return (
            t.isFunctionDeclaration(node) &&
            t.isBlockStatement(node.body) &&
            node.body.body.length === 3 &&
            (0, declaration_1.isDeclarationOrAssignmentStatement)(
                node.body.body[0],
                t.isIdentifier,
                (node) =>
                    t.isArrayExpression(node) || // explicit string array
                    (t.isCallExpression(node) && // creating string array by splitting a string
                        t.isMemberExpression(node.callee) &&
                        t.isStringLiteral(node.callee.object) &&
                        t.isIdentifier(node.callee.property) &&
                        node.callee.property.name === "split" &&
                        node.arguments.length === 1 &&
                        t.isStringLiteral(node.arguments[0])),
            ) &&
            (0, declaration_1.isDeclarationOrAssignmentStatement)(
                node.body.body[1],
                t.isIdentifier,
                (node) =>
                    t.isFunctionExpression(node) &&
                    t.isBlockStatement(node.body) &&
                    node.body.body.length === 1 &&
                    t.isReturnStatement(node.body.body[0]) &&
                    t.isIdentifier(node.body.body[0].argument),
            ) &&
            t.isReturnStatement(node.body.body[2]) &&
            t.isCallExpression(node.body.body[2].argument) &&
            t.isIdentifier(node.body.body[2].argument.callee) &&
            node.body.body[2].argument.arguments.length === 0
        );
    }
    /**
     * Returns whether a node is a basic string array wrapper function.
     * @param node The AST node.
     * @param stringArrayName The name of the string array function.
     * @returns Whether.
     */
    isBasicStringArrayWrapper(node, stringArrayName) {
        return (
            t.isFunctionDeclaration(node) &&
            t.isBlockStatement(node.body) &&
            node.body.body.length === 3 &&
            (0, declaration_1.isDeclarationOrAssignmentStatement)(
                node.body.body[0],
                t.isIdentifier,
                (node) =>
                    t.isCallExpression(node) &&
                    t.isIdentifier(node.callee) &&
                    node.callee.name === stringArrayName &&
                    node.arguments.length === 0,
            ) &&
            (0, declaration_1.isDeclarationOrAssignmentStatement)(
                node.body.body[1],
                t.isIdentifier,
                (node) =>
                    t.isFunctionExpression(node) &&
                    t.isBlockStatement(node.body) &&
                    node.body.body.length === 3 &&
                    (0, declaration_1.isDeclarationOrAssignmentStatement)(
                        node.body.body[0],
                        t.isIdentifier,
                        (node) =>
                            t.isBinaryExpression(node) &&
                            (node.operator === "-" || node.operator === "+") &&
                            t.isIdentifier(node.left) &&
                            t.isNumericLiteral(node.right),
                    ) &&
                    (0, declaration_1.isDeclarationOrAssignmentStatement)(
                        node.body.body[1],
                        t.isIdentifier,
                        (node) =>
                            t.isMemberExpression(node) &&
                            t.isIdentifier(node.object) &&
                            t.isIdentifier(node.property),
                    ) &&
                    t.isReturnStatement(node.body.body[2]) &&
                    t.isIdentifier(node.body.body[2].argument),
            ) &&
            t.isReturnStatement(node.body.body[2]) &&
            t.isCallExpression(node.body.body[2].argument) &&
            t.isIdentifier(node.body.body[2].argument.callee) &&
            node.body.body[2].argument.arguments.length === 2 &&
            t.isIdentifier(node.body.body[2].argument.arguments[0]) &&
            t.isIdentifier(node.body.body[2].argument.arguments[1])
        );
    }
    /**
     * Returns whether a node is either a base 64 or RC4 string array wrapper function.
     * @param node The AST node.
     * @param stringArrayName The name of the string array function.
     * @returns Whether.
     */
    isComplexStringArrayWrapper(node, stringArrayName) {
        return (
            t.isFunctionDeclaration(node) &&
            t.isBlockStatement(node.body) &&
            node.body.body.length === 3 &&
            (0, declaration_1.isDeclarationOrAssignmentStatement)(
                node.body.body[0],
                t.isIdentifier,
                (node) =>
                    t.isCallExpression(node) &&
                    t.isIdentifier(node.callee) &&
                    node.callee.name === stringArrayName &&
                    node.arguments.length === 0,
            ) &&
            (0, declaration_1.isDeclarationOrAssignmentStatement)(
                node.body.body[1],
                t.isIdentifier,
                (node) =>
                    t.isFunctionExpression(node) &&
                    t.isBlockStatement(node.body) &&
                    node.body.body.length >= 4 &&
                    (0, declaration_1.isDeclarationOrAssignmentStatement)(
                        node.body.body[0],
                        t.isIdentifier,
                        (node) =>
                            t.isBinaryExpression(node) &&
                            (node.operator === "-" || node.operator === "+") &&
                            t.isIdentifier(node.left) &&
                            t.isNumericLiteral(node.right),
                    ) &&
                    (0, declaration_1.isDeclarationOrAssignmentStatement)(
                        node.body.body[1],
                        t.isIdentifier,
                        (node) =>
                            t.isMemberExpression(node) &&
                            t.isIdentifier(node.object) &&
                            t.isIdentifier(node.property),
                    ) &&
                    t.isIfStatement(node.body.body[2]) &&
                    t.isIfStatement(
                        node.body.body[node.body.body.length - 2],
                    ) &&
                    t.isReturnStatement(
                        node.body.body[node.body.body.length - 1],
                    ),
            ) &&
            t.isReturnStatement(node.body.body[2]) &&
            t.isCallExpression(node.body.body[2].argument) &&
            t.isIdentifier(node.body.body[2].argument.callee) &&
            node.body.body[2].argument.arguments.length === 2 &&
            t.isIdentifier(node.body.body[2].argument.arguments[0]) &&
            node.body.body[2].argument.arguments[0].name === "arguments" &&
            t.isIdentifier(node.body.body[2].argument.arguments[1])
        );
    }
    /**
     * Returns whether a node is a call to rotate the string array.
     * @param node The AST node.
     * @param stringArrayName The name of the string array function.
     * @returns Whether.
     */
    isRotateStringArrayCall(node, stringArrayName) {
        return (
            t.isCallExpression(node) &&
            node.arguments.length === 2 &&
            t.isIdentifier(node.arguments[0]) &&
            node.arguments[0].name === stringArrayName &&
            t.isNumericLiteral(node.arguments[1]) &&
            t.isFunctionExpression(node.callee) &&
            t.isBlockStatement(node.callee.body) &&
            ((node.callee.body.body.length === 1 &&
                t.isForStatement(node.callee.body.body[0]) &&
                node.callee.body.body[0].init !== undefined &&
                (0, declaration_1.isDeclarationOrAssignmentExpression)(
                    node.callee.body.body[0].init,
                    t.isIdentifier,
                    (node) =>
                        t.isCallExpression(node) &&
                        t.isIdentifier(node.callee) &&
                        node.arguments.length === 0,
                ) &&
                node.callee.body.body[0].test !== undefined &&
                t.isBooleanLiteral(node.callee.body.body[0].test) &&
                node.callee.body.body[0].test.value) ||
                (node.callee.body.body.length === 2 &&
                    (0, declaration_1.isDeclarationOrAssignmentStatement)(
                        node.callee.body.body[0],
                        t.isIdentifier,
                        (node) =>
                            t.isCallExpression(node) &&
                            t.isIdentifier(node.callee) &&
                            node.arguments.length === 0,
                    ) &&
                    t.isWhileStatement(node.callee.body.body[1]) &&
                    t.isBooleanLiteral(node.callee.body.body[1].test) &&
                    node.callee.body.body[1].test.value === true))
        );
    }
    /**
     * Returns whether a node is a call of the string array wrapper function.
     * @param node The AST node.
     * @param wrapperType The type of string wrapper.
     * @returns Whether.
     */
    isStringArrayWrapperCall(node, wrapperType) {
        return (
            t.isCallExpression(node) &&
            t.isIdentifier(node.callee) &&
            ((wrapperType === stringDecoder_1.DecoderType.RC4 &&
                node.arguments.length === 2 &&
                t.isNumericLiteral(node.arguments[0]) &&
                t.isStringLiteral(node.arguments[1])) ||
                (wrapperType !== stringDecoder_1.DecoderType.RC4 &&
                    (node.arguments.length === 1 ||
                        node.arguments.length === 2) &&
                    t.isNumericLiteral(node.arguments[0])))
        );
    }
    /**
     * Returns whether a node is an escaped string literal.
     * @param node The AST node.
     * @returns Whether.
     */
    isEscapedStringLiteral(node) {
        return (
            t.isStringLiteral(node) &&
            node.extra !== undefined &&
            typeof node.extra.rawValue === "string" &&
            typeof node.extra.raw === "string" &&
            node.extra.raw.replace(/["']/g, "") !== node.extra.rawValue
        );
    }
}
exports.StringRevealer = StringRevealer;
StringRevealer.properties = {
    key: "stringRevealing",
    rebuildScopeTree: true,
};
